package redis.clients.jedis;

import java.net.URI;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocketFactory;

import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import redis.clients.jedis.exceptions.InvalidURIException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.JedisURIHelper;

/**
 * JedisFactory是Redis的一个工厂类，它实现了PooledObjectFactory接口，
 * 主要负责管理（JedisFactory是Redis的一个工厂类，它实现了PooledObjectFactory接口，
 * 主要负责管理（创建，关闭，验证）Redis的连接客户端jedis。
 * <p>
 * 从连接池获取jedis连接资源，实际上看是从JedisPool的父类pool中获取，而pool又委托给JedisFactory，
 * 最后由JedisFactory创建redis连接客户端jedis。
 */
public class JedisFactory implements PooledObjectFactory<Jedis> {

    private static final Logger logger = LoggerFactory.getLogger(JedisFactory.class);

    /**
     * 创建 Jedis 连接的工厂类, 将网络连接封装成 Jedis 对象
     */
    private final JedisSocketFactory jedisSocketFactory;

    /**
     * jedis 客户端配置接口
     */
    private final JedisClientConfig clientConfig;


    /**
     * 构建jedis工厂类 没有用户名
     */
    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final String password,
            final int database,
            final String clientName) {
        this(host, port, connectionTimeout, soTimeout, password, database, clientName, false, null, null, null);
    }


    /**
     * 构建jedis工厂类 infiniteSoTimeout == 0, 没有连接池中连接的 socket 超时时间
     */
    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName) {
        this(host, port, connectionTimeout, soTimeout, 0, user, password, database, clientName);
    }


    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final int infiniteSoTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName) {
        this(host, port, connectionTimeout, soTimeout, infiniteSoTimeout, user, password, database, clientName, false, null, null, null);
    }


    /**
     * {@link #setHostAndPort(redis.clients.jedis.HostAndPort) setHostAndPort} must be called later.
     */
    protected JedisFactory(
            final int connectionTimeout,
            final int soTimeout,
            final int infiniteSoTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName) {
        this(connectionTimeout, soTimeout, infiniteSoTimeout, user, password, database, clientName, false, null, null, null);
    }


    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final String password,
            final int database,
            final String clientName,
            final boolean ssl,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        this(host, port, connectionTimeout, soTimeout, null, password, database, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
    }


    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName,
            final boolean ssl,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        this(host, port, connectionTimeout, soTimeout, 0, user, password, database, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
    }


    /**
     * {@link #setHostAndPort(redis.clients.jedis.HostAndPort) setHostAndPort} must be called later.
     */
    protected JedisFactory(
            final int connectionTimeout,
            final int soTimeout,
            final int infiniteSoTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName,
            final boolean ssl,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        this(
                DefaultJedisClientConfig.builder()
                        .connectionTimeoutMillis(connectionTimeout)
                        .socketTimeoutMillis(soTimeout)
                        .blockingSocketTimeoutMillis(infiniteSoTimeout)
                        .user(user)
                        .password(password)
                        .database(database)
                        .clientName(clientName)
                        .ssl(ssl)
                        .sslSocketFactory(sslSocketFactory)
                        .sslParameters(sslParameters)
                        .hostnameVerifier(hostnameVerifier)
                        .build());
    }


    protected JedisFactory(
            final URI uri,
            final int connectionTimeout,
            final int soTimeout,
            final String clientName) {
        this(uri, connectionTimeout, soTimeout, clientName, null, null, null);
    }


    protected JedisFactory(
            final URI uri,
            final int connectionTimeout,
            final int soTimeout,
            final String clientName,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        this(uri, connectionTimeout, soTimeout, 0, clientName, sslSocketFactory, sslParameters, hostnameVerifier);
    }


    /**
     * 最终设置
     */
    protected JedisFactory(
            final String host,
            final int port,
            final int connectionTimeout,
            final int soTimeout,
            final int infiniteSoTimeout,
            final String user,
            final String password,
            final int database,
            final String clientName,
            final boolean ssl,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        this.clientConfig =
                DefaultJedisClientConfig.builder()
                        .connectionTimeoutMillis(connectionTimeout)
                        .socketTimeoutMillis(soTimeout)
                        .blockingSocketTimeoutMillis(infiniteSoTimeout)
                        .user(user)
                        .password(password)
                        .database(database)
                        .clientName(clientName)
                        .ssl(ssl)
                        .sslSocketFactory(sslSocketFactory)
                        .sslParameters(sslParameters)
                        .hostnameVerifier(hostnameVerifier).build();
        this.jedisSocketFactory = new DefaultJedisSocketFactory(new HostAndPort(host, port), this.clientConfig);
    }


    /**
     * 最终设置
     */
    protected JedisFactory(
            final HostAndPort hostAndPort,
            final JedisClientConfig clientConfig) {
        this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig);
        this.jedisSocketFactory = new DefaultJedisSocketFactory(hostAndPort, this.clientConfig);
    }

    protected JedisFactory(
            final JedisSocketFactory jedisSocketFactory,
            final JedisClientConfig clientConfig) {
        this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig);
        this.jedisSocketFactory = jedisSocketFactory;
    }

    protected JedisFactory(final JedisClientConfig clientConfig) {
        this.clientConfig = clientConfig;
        this.jedisSocketFactory = new DefaultJedisSocketFactory(clientConfig);
    }


    /**
     * 最终设置
     */
    protected JedisFactory(
            final URI uri,
            final int connectionTimeout,
            final int soTimeout,
            final int infiniteSoTimeout,
            final String clientName,
            final SSLSocketFactory sslSocketFactory,
            final SSLParameters sslParameters,
            final HostnameVerifier hostnameVerifier) {
        if (!JedisURIHelper.isValid(uri)) {
            throw new InvalidURIException(String.format(
                    "Cannot open Redis connection due invalid URI. %s", uri.toString()));
        }
        this.clientConfig = DefaultJedisClientConfig.builder().connectionTimeoutMillis(connectionTimeout)
                .socketTimeoutMillis(soTimeout).blockingSocketTimeoutMillis(infiniteSoTimeout)
                .user(JedisURIHelper.getUser(uri)).password(JedisURIHelper.getPassword(uri))
                .database(JedisURIHelper.getDBIndex(uri)).clientName(clientName)
                .ssl(JedisURIHelper.isRedisSSLScheme(uri)).sslSocketFactory(sslSocketFactory)
                .sslParameters(sslParameters).hostnameVerifier(hostnameVerifier).build();
        this.jedisSocketFactory = new DefaultJedisSocketFactory(new HostAndPort(uri.getHost(), uri.getPort()), this.clientConfig);
    }


    public void setHostAndPort(final HostAndPort hostAndPort) {
        jedisSocketFactory.updateHostAndPort(hostAndPort);
    }

    public void setPassword(final String password) {
        this.clientConfig.updatePassword(password);
    }


    /**
     * 激活一个Jedis实例
     * <p>
     * 方法会在Jedis实例被从JedisFactory的activateObject方法用于激活一个Jedis实例，
     * 该方法会在Jedis实例被从连接池中取出并返回给调用者之前被调用。
     * <p>
     * 检查Jedis实例是否已经被关闭；
     * 如果Jedis实例未被关闭，则将其标记为已激活；
     * 如果Jedis实例未被激活，则尝试重新连接到Redis服务器；
     * 如果Jedis实例已经连接到Redis服务器，但当前使用的数据库不是预期的数据库，则切换到预期的数据库。
     */
    @Override
    public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
        final BinaryJedis jedis = pooledJedis.getObject();
        if (jedis.getDB() != clientConfig.getDatabase()) {
            jedis.select(clientConfig.getDatabase());
        }
    }


    /**
     * 销毁一个Jedis实例
     * <p>
     * 该方法会在Jedis实例被归还JedisFactory的destroyObject方法用于销毁一个Jedis实例，
     * 该方法会在Jedis实例被归还到JedisFactory之前被调用
     * <p>
     * 在destroyObject方法中，通常会执行一些清理操作，
     * 例如关闭Jedis实例、释放资源等。这些操作可以确保Jedis实例被正确地回收和清理，
     * 避免资源浪费和内存泄漏等问题
     * <p>
     * 检查Jedis实例是否已经被关闭；
     * 如果Jedis实例未被关闭，则关闭它；
     * 将Jedis实例标记为已销毁；
     * 将Jedis实例从连接池中移除。
     */
    @Override
    public void destroyObject(PooledObject<Jedis> pooledJedis) throws Exception {
        final BinaryJedis jedis = pooledJedis.getObject();
        if (jedis.isConnected()) {
            try {
                jedis.close();
            } catch (RuntimeException e) {
                logger.debug("Error while close", e);
            }
        }
    }


    /**
     * 创建一个新的Jedis实例
     * <p>
     * 通常会执行一些初始化操作，例如创建一个新的Jedis对象、设置连接参数等。
     * 这些操作可以确保新创建的Jedis实例处于可用状态，并且可以被安全地使用
     * <p>
     * 创建一个新的Jedis对象；
     * 设置连接参数，如host、port、timeout等；
     * 如果需要，还可以设置用户名和密码；
     * 返回新创建的Jedis对象。
     */
    @Override
    public PooledObject<Jedis> makeObject() throws Exception {
        Jedis jedis = null;
        try {
            jedis = new Jedis(jedisSocketFactory, clientConfig);
            jedis.connect();
            return new DefaultPooledObject<>(jedis);
        } catch (JedisException je) {
            if (jedis != null) {
                try {
                    jedis.close();
                } catch (RuntimeException e) {
                    logger.debug("Error while close", e);
                }
            }
            throw je;
        }
    }

    @Override
    public void passivateObject(PooledObject<Jedis> pooledJedis) throws Exception {
        // TODO maybe should select db 0? Not sure right now.
    }


    /**
     * 验证一个Jedis实例是否有效
     * <p>
     * 通常会执行一些检查操作，例如ping命令、选择正确的数据库等。
     * 这些操作可以确保Jedis实例仍然连接到Redis服务器，并且当前使用的数据库是正确的
     * <p>
     * 执行ping命令来检查Jedis实例是否仍然连接到Redis服务器；
     * 如果Jedis实例未连接到Redis服务器，则抛出异常；
     * 如果Jedis实例已连接到Redis服务器，但当前使用的数据库不是预期的数据库，则切换到预期的数据库。
     */
    @Override
    public boolean validateObject(PooledObject<Jedis> pooledJedis) {
        final BinaryJedis jedis = pooledJedis.getObject();
        try {
            String host = jedisSocketFactory.getHost();
            int port = jedisSocketFactory.getPort();

            String connectionHost = jedis.getClient().getHost();
            int connectionPort = jedis.getClient().getPort();

            return host.equals(connectionHost)
                    && port == connectionPort && jedis.isConnected()
                    && jedis.ping().equals("PONG");
        } catch (final Exception e) {
            logger.error("Error while validating pooled Jedis object.", e);
            return false;
        }
    }
}
